Skip to content

使用 CallerArgumentExpression 簡化參數檢核

TLDR

  • 使用 [CallerArgumentExpression] 屬性,可以在呼叫方法時自動擷取傳入參數的變數名稱,無需手動傳入字串。
  • 相比使用 Expression<Func<T>> 的方式,此方法效能更好且程式碼更簡潔。
  • 配合 [NotNull][DoesNotReturn] 等屬性,能有效支援 .NET 的 Nullable reference type 靜態分析,解決編譯器誤報警告的問題。
  • 建議優先使用 .NET 6/7 內建的 ArgumentNullException.ThrowIfNull 等方法,若需自訂檢核邏輯,可參考此模式實作。

使用 Expression 進行參數檢核的問題

在開發套件時,為了簡化參數檢核並統一錯誤訊息,開發者常會建立 ExceptionUtils 工具類。傳統做法是利用 Expression 來擷取變數名稱,避免重複輸入參數名稱字串:

csharp
public static class ExceptionUtils {
    public static void ThrowIfNull<T>(Expression<Func<T?>> expression) {
        _ = expression.Compile().Invoke()
            ?? throw new ArgumentNullException(GetMemberName(expression));
    }

    private static string GetMemberName<T>(Expression<Func<T>> expression) {
        if (expression.Body is not MemberExpression expressionBody) {
            throw new ArgumentException("Expression 表達式錯誤。", nameof(expression));
        }
        return expressionBody.Member.Name;
    }
}

什麼情況下會遇到這個問題:當專案啟用 Nullable reference type 檢查時,上述使用 Expression 的方式無法讓編譯器辨識檢查後的變數不為 null,導致編譯器持續發出警告。此外,使用 Expression 需要編譯表達式,會帶來額外的效能開銷。

引入 CallerArgumentExpression 的解決方案

從 .NET 6 開始,官方引入了 [CallerArgumentExpression] 屬性。當方法參數標記此屬性時,編譯器會自動將呼叫端傳入該參數的「變數名稱」作為字串傳入。

實作範例

透過此屬性,我們可以重構檢核邏輯,使其既簡潔又能正確支援編譯器檢查:

csharp
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;

public static class ExceptionUtils {
    public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
        if (argument is null) {
            throw new ArgumentNullException(paramName);
        }
    }
}

驗證結果

測試程式碼如下:

csharp
string? str = "";
TestCallerArgumentExpression(str); // 未傳入 paramName
TestCallerArgumentExpression(str, "str2"); // 有傳入 paramName

void TestCallerArgumentExpression(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
    Console.WriteLine("paramName:" + paramName);
}

執行結果:

text
paramName: str
paramName: str2

什麼情況下會遇到這個問題:當需要自訂檢核邏輯(例如檢查特定格式或範圍),且希望在拋出例外時自動帶入變數名稱,同時保持編譯器對 Nullable reference type 的正確判斷時。

結論與建議

  1. 優先使用內建方法:.NET 6 與 .NET 7 已經內建了 ArgumentNullException.ThrowIfNullArgumentException.ThrowIfNullOrEmpty 等方法,這些方法內部均已實作 [CallerArgumentExpression],應優先採用。
  2. 效能優勢:使用 [CallerArgumentExpression] 取代 Expression<Func<T>>,可以避免執行時期的 Compile()Invoke() 操作,顯著提升效能。
  3. 編譯器支援:透過搭配 [NotNull][DoesNotReturn] 等屬性,可以讓編譯器正確識別程式碼的執行路徑,減少不必要的 Nullable 警告。

異動歷程

    • 初版文件建立。